package upload

import (
	"bytes"
	"context"
	"fmt"
	"gitee.com/bobo-rs/creative-framework/pkg/encrypt"
	"gitee.com/bobo-rs/creative-framework/pkg/ifile"
	"gitee.com/bobo-rs/creative-framework/pkg/sredis"
	"gitee.com/bobo-rs/creative-framework/pkg/utils"
	"gitee.com/bobo-rs/goffm/ffprobe"
	"gitee.com/bobo-rs/innovideo-services/consts"
	"gitee.com/bobo-rs/innovideo-services/framework/model"
	"gitee.com/bobo-rs/innovideo-services/framework/model/entity"
	"gitee.com/bobo-rs/innovideo-services/library/exception"
	"gitee.com/bobo-rs/innovideo-services/library/tools"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gfile"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

// GetUploadBlockSpace 获取上传空间文件块
func (u *sUpload) GetUploadBlockSpace(ctx context.Context) (out *model.UploadBlockSpaceOutput, err error) {
	out = &model.UploadBlockSpaceOutput{}
	out.Hash = encrypt.NewM().MustEncryptString(utils.CreateToken(strconv.Itoa(utils.RandInt(8))))
	// 缓存
	err = sredis.NewCache().Set(ctx, tools.CacheKey(consts.UploadBlockSpace, out.Hash), out.Hash, time.Hour)
	if err != nil {
		return nil, fmt.Errorf(`创建文件空间失败%w`, err)
	}
	return
}

// UploadBlockToFileSystem 文件分块上传到文件系统
func (u *sUpload) UploadBlockToFileSystem(ctx context.Context, in model.UploadBlockInput) (out *model.UploadBlockOutput, err error) {
	out = &model.UploadBlockOutput{}
	defer func() {
		if err != nil {
			g.Log().Debug(ctx, `上传文件块失败`, err)
			return
		}
		// 上传成功临时文件
		g.Log().Info(ctx, `上传成功`, out.Url)
	}()

	// 检测空间标识是否存在
	if b, _ := sredis.NewCache().Contains(ctx, tools.CacheKey(consts.UploadBlockSpace, in.FileHash)); !b {
		return nil, exception.New(`上传文件块空间未分配或已失效`)
	}

	// 获取Block字典
	blockMap, err := u.GetUploadBlockFileIndexMap(ctx, in.FileHash)
	if err != nil {
		return nil, err
	}

	// 获取文件主目录
	mainDir, err := u.BlockMainTmpDir(ctx, in.FileHash)
	if err != nil {
		return nil, err
	}

	blockHash := encrypt.NewM().MustEncrypt(in.Block)
	// Block已上传
	if _, ok := blockMap[blockHash]; ok {
		out.Url = u.tmpFilePath(mainDir, blockHash)
		return
	}

	// 内容临时保存
	out.Url, err = u.CreateBlockSubTmpFile(ctx, mainDir, blockHash, func(ctx context.Context, file *os.File) error {
		// 写入内容
		if _, err = io.Copy(file, bytes.NewReader(in.Block)); err != nil {
			return fmt.Errorf(`上传内容写入失败%w`, err)
		}
		return err
	})
	if err != nil {
		return nil, err
	}

	// 写入对应索引
	_, err = u.SetUploadBlockFileIndex(ctx, model.UploadBlockItem{
		Index:     in.Index,
		Hash:      in.FileHash,
		BlockHash: blockHash,
		Size:      len(in.Block),
	})
	if err != nil {
		return nil, err
	}

	// 是否最后一个文件块，false否，true最后一个（需要合并）
	if !in.IsLast {
		return
	}

	// 合并文件
	err = u.SetUploadBlockIsCompleted(ctx, in.FileHash, in.UploadBlockFile)
	if err != nil {
		return nil, fmt.Errorf(`设置文件上传完成失败，请重试 %w`, err)
	}
	// 记录日志
	g.Log().Info(ctx, `文件块已上传完成，等待合成`)
	return
}

// MergeUploadedBlocks 合并已上传文件块
func (u *sUpload) MergeUploadedBlocks(ctx context.Context, hash string) (out *model.MergeUploadBlockOutput, err error) {
	out = &model.MergeUploadBlockOutput{}
	defer func() {
		if err != nil {
			g.Log().Debug(ctx, `文件合成失败`, err)
			return
		}
		g.Log().Info(ctx, `文件合成成功`, out.Url)
	}()

	// 获取上传完成信息
	item, err := u.GetUploadBlockIsCompleted(ctx, hash)
	if err != nil {
		return nil, err
	}

	// 获取上传索引
	blocks, err := u.GetUploadBlockFileIndex(ctx, hash)
	if err != nil {
		return nil, exception.Newf(`未检测到已上传的文件分块%s`, err.Error())
	}

	// 合并文件
	mergeInfo, err := u.InternalMergeBlocks(ctx, model.UploadBlockMerge{
		Filename: item.Filename,
		FileHash: hash,
		MimeType: item.MimeType,
		Blocks:   blocks,
	})
	if err != nil {
		return nil, err
	}

	// 保存合并后的视频附件
	if err = u.SaveMergeVideoFile(ctx, *mergeInfo, *item); err != nil {
		return nil, err
	}

	// 删除整个临时数据
	_ = u.removeMergeTmpBlocks(ctx, hash)
	out.Url = mergeInfo.Url
	return
}

// CreateBlockSubTmpFile 创建并处理临时文件
func (u *sUpload) CreateBlockSubTmpFile(ctx context.Context, mainDir, blockHash string, f func(ctx context.Context, file *os.File) error) (string, error) {
	// 创建并打开文件
	file, err := os.Create(u.tmpFilePath(mainDir, blockHash))
	if err != nil {
		return "", err
	}
	defer func() {
		_ = file.Close()
	}()
	// 处理文件
	err = f(ctx, file)
	if err != nil {
		return "", err
	}
	return u.tmpFilePath(mainDir, blockHash), nil
}

// GetUploadBlockFileIndexMap 获取上传历史文件块索引字典
func (u *sUpload) GetUploadBlockFileIndexMap(ctx context.Context, hash string) (map[string]model.UploadBlockItem, error) {
	blocks, err := u.GetUploadBlockFileIndex(ctx, hash)
	if err != nil {
		return nil, err
	}
	// 转换为字典
	blockMap := make(map[string]model.UploadBlockItem)
	for _, item := range blocks {
		blockMap[item.BlockHash] = item
	}
	return blockMap, nil
}

// BlockMainTmpDir 获取或创建主文件临时目录
func (u *sUpload) BlockMainTmpDir(ctx context.Context, fileHash string) (string, error) {
	// 创建临时目录
	return u.CreateFileDir(ctx, `tmp`, fileHash)
}

// BlockFileDir 创建文件目录
func (u *sUpload) BlockFileDir(ctx context.Context, fileExt string) (string, error) {
	// 创建文件目录
	return u.CreateFileDir(ctx, fileExt, time.Now().Format(`20060102`))
}

// CreateFileDir 创建文件目录
func (u *sUpload) CreateFileDir(ctx context.Context, dirs ...string) (string, error) {
	// 获取配置
	config, err := u.LocalUploadConfig(ctx)
	if err != nil {
		return "", err
	}
	dir := ifile.SlashUploadPath(fmt.Sprintf(`%s/%s`, config.Path, strings.Join(dirs, `/`)))
	// 检测文件是否存在
	if !gfile.IsDir(dir) {
		if err = gfile.Mkdir(dir); err != nil {
			return "", err
		}
	}
	return dir, nil
}

// InternalMergeBlocks 合并文件块（内部使用）
func (u *sUpload) InternalMergeBlocks(ctx context.Context, item model.UploadBlockMerge) (info *model.UploadBlockGenMerge, err error) {
	mu := sync.Mutex{}
	mu.Lock()
	defer mu.Unlock()
	// 初始化
	info, err = u.prepareMergeFileInfo(ctx, item)
	if err != nil {
		return nil, err
	}

	// 信息是否存在
	if info.Hashed != `` {
		return info, nil
	}
	defer func() {
		if err != nil {
			// 合并文件中途失败，则回收掉已生成文件
			_ = os.Remove(info.Url)
		}
	}()

	// 获取文件块临时目录
	tmpDir, err := u.BlockMainTmpDir(ctx, item.FileHash)
	if err != nil {
		return nil, err
	}

	// 创建合并主文件
	file, err := os.Create(filepath.Join(info.Url))
	if err != nil {
		return nil, fmt.Errorf(`创建合并文件失败%w`, err)
	}

	// 关闭文件
	defer func() { _ = file.Close() }()

	// 迭代合并文件
	for _, block := range item.Blocks {
		// 合并文件
		err = u.mergeTempFileIntoMainFile(file, tmpDir, block.BlockHash)
		if err != nil {
			return nil, err
		}
	}

	// 以文件内容生成HASH值和附件ID
	info.Hashed, info.AttachId, err = u.fileHashedAndAttachId(file)
	if err != nil {
		return nil, err
	}

	// 设置合并成功缓存
	_ = u.SetUploadBlockMergeCompleted(ctx, item.FileHash, info)
	return
}

// mergeTempFileIntoMainFile 将临时文件合并到主文件中
func (u sUpload) mergeTempFileIntoMainFile(mainFile *os.File, tmpDir, blockHash string) error {
	// 读取临时文件内容
	tmpFile, err := os.Open(filepath.Join(tmpDir, blockHash) + `.tmp`)
	if err != nil {
		return fmt.Errorf(`打开临时文件失败%w`, err)
	}
	defer func() {
		_ = tmpFile.Close()
	}()

	// 写入文件
	if _, err = io.Copy(mainFile, tmpFile); err != nil {
		return fmt.Errorf(`临时文件写入失败%w`, err)
	}
	return nil
}

// removeMergeTmpBlocks 移除合并后的临时文件或块的索引
func (u sUpload) removeMergeTmpBlocks(ctx context.Context, fileHash string) error {
	// 获取文件块临时目录
	tmpDir, err := u.BlockMainTmpDir(ctx, fileHash)
	if err != nil {
		return err
	}

	// 移除整个临时文件目录
	if err := os.RemoveAll(tmpDir); err != nil {
		g.Log().Debug(ctx, `删除整个临时目录失败`, tmpDir, err)
	}

	return u.RemoveUploadBlockFileCache(ctx, fileHash)
}

// extractVideoMetadata 提取视频文件的元数据
func (u *sUpload) extractVideoMetadata(attach *entity.Attachment) error {
	path, err := filepath.Abs(attach.AttPath)
	if err != nil {
		return fmt.Errorf(`解析文件地址出错%w`, err)
	}

	var (
		probeHandler = ffprobe.New().Src(path)
		item         = probeHandler.MustFormat()
		videoStream  = probeHandler.MustVideoStream()
		t            = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Now().Location())
	)

	attach.AttSize = item.Size
	attach.AttType = `video/` + gfile.ExtName(attach.AttPath)
	attach.AttSize = item.Size
	attach.Name = gfile.Basename(item.Filename)
	attach.Width = uint(videoStream.Width)
	attach.Height = uint(videoStream.Height)
	attach.StorageType = localStorageDriver
	attach.Duration = uint(item.Duration)

	// 格式化时长
	attach.DurationString = t.Add(time.Duration(int64(attach.Duration)) * time.Second).Format(time.TimeOnly)
	// 生成哈希值
	if attach.Hasher != `` {
		return nil
	}

	// HASH值为空，生成哈希值
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf(`打开视频文件失败%w`, err)
	}
	attach.Hasher, attach.AttachId, err = u.fileHashedAndAttachId(file)

	return err
}

// tmpFilePath 生成临时文件路径
func (u *sUpload) tmpFilePath(dir, blockHash string) string {
	return fmt.Sprintf(`%s/%s.tmp`, dir, blockHash)
}

// prepareMergeFileInfo 准备并检测合并文件信息
func (u *sUpload) prepareMergeFileInfo(ctx context.Context, item model.UploadBlockMerge) (*model.UploadBlockGenMerge, error) {
	// 获取并检测是否之前已经合并完成
	info, err := u.GetUploadBlockMergeCompleted(ctx, item.FileHash)
	if err != nil {
		return nil, err
	}

	// 之前已完成
	if info != nil && gfile.Exists(info.Url) {
		return info, nil
	}

	// 创建文件目录
	dir, err := u.BlockFileDir(ctx, gfile.ExtName(item.Filename))
	if err != nil {
		return nil, err
	}

	// 生成合并文件路径
	info.Url = ifile.SlashUploadPath(fmt.Sprintf(`%s/%s%s`, dir, item.FileHash, filepath.Ext(item.Filename)))
	return info, nil
}
